
"""
Application class here
该模块定义了应用程序的主类，负责管理应用程序的各种功能，
如文件操作、菜单管理、工具管理等。
"""

import os
import json
import shutil
from string import ascii_letters
import random
from datetime import datetime

from PySide6.QtGui import QAction
from PySide6.QtWidgets import QMessageBox, QFileDialog, QMenuBar, QToolBar, QTabWidget
from qtpy import QtGui
from qtpy import QtCore
from qtpy.QtWidgets import QMenu
from qtpy.QtWidgets import QMainWindow

from src.UI.EditorHistory import EditorHistory
from src import GET_PACKAGES
from src.Core.PathsRegistry import PathsRegistry
from src.Core.version import *
from src.Core.GraphManager import GraphManagerSingleton
from src.Core.Common import currentProcessorTime
from src.Core.Common import SingletonDecorator
from src.Core.Common import validateGraphDataPackages
from src.UI.Canvas.UICommon import SessionDescriptor
from src.UI.Widgets.BlueprintCanvas import BlueprintCanvasWidget
from src.UI.Tool.Tool import ShelfTool, DockTool
from src.UI.EditorHistory import EditorHistory
from src.UI.Tool import GET_TOOLS
from src.UI.Utils.stylesheet import editableStyleSheet
from src.UI.ContextMenuGenerator import ContextMenuGenerator
from src.UI.Widgets.PreferencesWindow import PreferencesWindow

try:
    from src.Packages.PyFlowBase.Tools.PropertiesTool import PropertiesTool
except ImportError:
    pass
import asyncio

import src.UI.resources
from src.Wizards.PackageWizard import PackageWizard

from src import INITIALIZE
from src.Input import InputAction, InputActionType
from src.Input import InputManager
from src.ConfigManager import ConfigManager

import src.UI.resources

# 编辑器目标帧率
EDITOR_TARGET_FPS = 30


def generateRandomString(numbSymbols=5):
    """
    生成指定长度的随机字符串。

    :param numbSymbols: 随机字符串的长度，默认为 5
    :return: 生成的随机字符串
    """
    result = ""
    for i in range(numbSymbols):
        # 从字母表中随机选择一个字母
        letter = random.choice(ascii_letters)
        result += letter
    return result


def getOrCreateMenu(menuBar, title):
    """
    在菜单栏中获取或创建一个菜单。

    :param menuBar: 菜单栏对象
    :param title: 菜单的标题
    :return: 找到或创建的菜单对象
    """
    for child in menuBar.findChildren(QMenu):
        if child.title() == title:
            return child
    # 创建新的菜单
    menu = QMenu(menuBar)
    menu.setObjectName(title)
    menu.setTitle(title)
    return menu


def winTitle():
    """
    获取应用程序窗口的标题，包含版本信息。

    :return: 应用程序窗口的标题
    """
    return "服务编排v0.1"


# App itself
class PySC(QMainWindow):
    """
    PySC 应用程序的主类，继承自 QMainWindow。
    负责管理应用程序的界面、文件操作、工具管理等功能。
    """
    # 应用程序实例
    appInstance = None

    # 新文件创建信号
    newFileExecuted = QtCore.Signal(bool)
    # 文件加载完成信号
    fileBeenLoaded = QtCore.Signal()

    def __init__(self, parent=None):
        """
        初始化 PySC 应用程序。

        :param parent: 父窗口对象，默认为 None
        """
        super(PySC, self).__init__(parent=parent)
        # 标记文件是否被修改
        self._modified = False
        # 设置焦点策略为强焦点
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        # 当前运行的软件名称
        self.currentSoftware = ""
        # 编辑器历史记录对象
        self.edHistory = EditorHistory(self)
        # 连接历史状态推送信号到处理函数
        self.edHistory.statePushed.connect(self.historyStatePushed)
        # 设置窗口标题
        self.setWindowTitle(winTitle())
        # 撤销栈对象
        self.undoStack = QtGui.QUndoStack(self)
        # 设置窗口内容边距
        self.setContentsMargins(1, 1, 1, 1)
        # 获取图管理器单例
        self.graphManager = GraphManagerSingleton()
        # 创建蓝图画布部件
        self.canvasWidget = BlueprintCanvasWidget(self.graphManager.get(), self)
        self.canvasWidget.setObjectName("canvasWidget")
        # 设置中心部件为画布部件
        self.setCentralWidget(self.canvasWidget)
        # 设置停靠窗口的标签位置
        self.setTabPosition(QtCore.Qt.AllDockWidgetAreas, QTabWidget.North)
        # 设置停靠窗口的选项
        self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)

        # 创建菜单栏
        self.menuBar = QMenuBar(self)
        self.menuBar.setGeometry(QtCore.QRect(0, 0, 863, 21))
        self.menuBar.setObjectName("menuBar")
        self.setMenuBar(self.menuBar)
        # 创建工具栏
        self.toolBar = QToolBar(self)
        self.toolBar.setObjectName("toolBar")
        # 将工具栏添加到窗口顶部
        self.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar)

        # 设置窗口图标
        self.setWindowIcon(QtGui.QIcon(":/LogoBpApp.png"))
        # 存储工具实例的集合
        self._tools = set()
        # 当前临时目录路径
        self.currentTempDir = ""

        # 首选项窗口对象
        self.preferencesWindow = PreferencesWindow(self)

        # 启用鼠标跟踪
        self.setMouseTracking(True)

        # 上一次时钟时间
        self._lastClock = 0.0
        # 当前帧率
        self.fps = EDITOR_TARGET_FPS
        # 定时器对象，用于主循环
        self.tick_timer = QtCore.QTimer()
        # 当前文件名
        self._currentFileName = ""
        self.currentFileName = None

    def historyStatePushed(self, state):
        """
        处理历史状态推送信号。

        :param state: 推送的历史状态对象
        """
        if state.modifiesData():
            # 如果状态修改了数据，标记文件已修改并更新标签
            self.modified = True
            self.updateLabel()
        # print(state, state.modifiesData())

    @property
    def modified(self):
        """
        获取文件是否被修改的标记。

        :return: 文件是否被修改的布尔值
        """
        return self._modified

    @modified.setter
    def modified(self, value):
        """
        设置文件是否被修改的标记，并更新窗口标签。

        :param value: 文件是否被修改的布尔值
        """
        self._modified = value
        self.updateLabel()

    def updateLabel(self):
        """
        更新窗口标题，显示当前文件名和是否被修改的标记。
        """
        label = "Untitled"
        if self.currentFileName is not None:
            if os.path.isfile(self.currentFileName):
                # 获取文件名作为标签
                label = os.path.basename(self.currentFileName)
        if self.modified:
            # 如果文件被修改，添加星号标记
            label += "*"
        self.setWindowTitle("{0} - {1}".format(winTitle(), label))

    def getTempDirectory(self):
        """
        返回应用程序实例的唯一临时目录。
        该文件夹及其所有内容将在应用程序关闭时从磁盘中删除。

        :return: 临时目录的路径
        """
        if self.currentTempDir == "":
            # 从配置管理器中获取临时文件目录路径
            tempDirPath = ConfigManager().getPrefsValue("PREFS", "General/TempFilesDir")
            if tempDirPath[-1:] in ("/", "\\"):
                # 去除路径末尾的斜杠
                tempDirPath = tempDirPath[:-1]
            # 生成包含随机字符串的临时目录路径
            self.currentTempDir = "{0}_{1}".format(tempDirPath, generateRandomString())

            if not os.path.exists(self.currentTempDir):
                # 创建临时目录
                os.makedirs(self.currentTempDir)
        return self.currentTempDir

    def getMenuBar(self):
        """
        获取菜单栏对象。

        :return: 菜单栏对象
        """
        return self.menuBar

    def populateMenu(self):
        """
        填充应用程序的菜单栏，添加各种菜单项和操作。
        """
        # 文件菜单
        fileMenu = self.menuBar.addMenu("File")
        # 新建文件操作
        newFileAction = fileMenu.addAction("New file")
        newFileAction.setIcon(QtGui.QIcon(":/new_file_icon.png"))
        newFileAction.triggered.connect(self._clickNewFile)

        # 加载文件操作
        loadAction = fileMenu.addAction("Load")
        loadAction.setIcon(QtGui.QIcon(":/folder_open_icon.png"))
        loadAction.triggered.connect(self.load)

        # 保存文件操作
        saveAction = fileMenu.addAction("Save")
        saveAction.setIcon(QtGui.QIcon(":/save_icon.png"))
        saveAction.triggered.connect(self.save)

        # 另存为操作
        saveAsAction = fileMenu.addAction("Save as")
        saveAsAction.setIcon(QtGui.QIcon(":/save_as_icon.png"))
        saveAsAction.triggered.connect(lambda: self.save(True))

        # 重新加载包操作
        reloadPackages = fileMenu.addAction("Reload packages")
        reloadPackages.setIcon(QtGui.QIcon(":/new_file_icon.png"))
        reloadPackages.triggered.connect(self._clickReloadPackages)

        # 自定义输入输出菜单
        IOMenu = fileMenu.addMenu("Custom IO")
        for packageName, package in GET_PACKAGES().items():
            # 导出器
            try:
                exporters = package.GetExporters()
            except:
                continue
            # 为每个包创建子菜单
            pkgMenu = IOMenu.addMenu(packageName)
            for exporterName, exporterClass in exporters.items():
                # 为每个导出器创建子菜单
                fileFormatMenu = pkgMenu.addMenu(exporterClass.displayName())
                fileFormatMenu.setToolTip(exporterClass.toolTip())
                if exporterClass.createExporterMenu():
                    # 导出操作
                    exportAction = fileFormatMenu.addAction("Export")
                    exportAction.triggered.connect(
                        lambda checked=False, app=self, exporter=exporterClass: exporter.doExport(
                            app
                        )
                    )
                if exporterClass.createImporterMenu():
                    # 导入操作
                    importAction = fileFormatMenu.addAction("Import")
                    importAction.triggered.connect(
                        lambda checked=False, app=self, exporter=exporterClass: exporter.doImport(
                            app
                        )
                    )

        # 编辑菜单
        editMenu = self.menuBar.addMenu("Edit")
        # 首选项操作
        preferencesAction = editMenu.addAction("Preferences")
        preferencesAction.setIcon(QtGui.QIcon(":/options_icon.png"))
        preferencesAction.triggered.connect(self.showPreferencesWindow)

        # 插件菜单
        pluginsMenu = self.menuBar.addMenu("Plugins")
        # 创建包向导操作
        packagePlugin = pluginsMenu.addAction("Create package...")
        packagePlugin.triggered.connect(PackageWizard.run)

        # 帮助菜单
        helpMenu = self.menuBar.addMenu("Help")
        # 主页操作
        helpMenu.addAction("Homepage").triggered.connect(
            lambda _=False, url="https://pedrocabrera.github.io/PyFlow/": QtGui.QDesktopServices.openUrl(
                url
            )
        )
        # 文档操作
        helpMenu.addAction("Docs").triggered.connect(
            lambda _=False, url="https://pyflow.readthedocs.io/en/latest/": QtGui.QDesktopServices.openUrl(
                url
            )
        )

    def showPreferencesWindow(self):
        """
        显示首选项窗口。
        """
        self.preferencesWindow.show()

    def registerToolInstance(self, instance):
        """
        注册工具实例引用，防止工具类被垃圾回收，并保存部件状态。

        :param instance: 要注册的工具实例，类型为 ToolBase
        """
        self._tools.add(instance)

    def unregisterToolInstance(self, instance):
        """
        取消注册工具实例。

        :param instance: 要取消注册的工具实例
        """
        if instance in self._tools:
            self._tools.remove(instance)

    def onRequestFillProperties(self, propertiesFillDelegate):
        """
        请求填充属性工具的属性窗口。

        :param propertiesFillDelegate: 属性填充委托对象
        """
        for toolInstance in self._tools:
            if toolInstance.__class__.__name__ == "PropertiesTool":
                # 清空属性工具内容
                toolInstance.clear()
                # 分配属性填充委托
                toolInstance.assignPropertiesWidget(propertiesFillDelegate)

    def onRequestClearProperties(self):
        """
        请求清空属性工具的属性窗口。
        """
        for toolInstance in self._tools:
            if toolInstance.__class__.__name__ == "PropertiesTool":
                # 清空属性工具内容
                toolInstance.clear()

    def getToolbar(self):
        """
        获取工具栏对象。

        :return: 工具栏对象
        """
        return self.toolBar

    def getCanvas(self):
        """
        获取画布对象。

        :return: 画布对象
        """
        return self.canvasWidget.canvas

    def keyPressEvent(self, event):
        """
        处理键盘按键事件，根据按键执行相应的操作。

        :param event: 键盘事件对象
        """
        # 获取按键修饰符
        modifiers = event.modifiers()
        # 创建输入动作对象
        currentInputAction = InputAction(
            name="temp",
            actionType=InputActionType.Keyboard,
            key=event.key(),
            modifiers=modifiers,
        )

        # 获取保存、新建文件、加载文件、另存为操作的输入动作变体
        actionSaveVariants = InputManager()["App.Save"]
        actionNewFileVariants = InputManager()["App.NewFile"]
        actionLoadVariants = InputManager()["App.Load"]
        actionSaveAsVariants = InputManager()["App.SaveAs"]

        if currentInputAction in actionNewFileVariants:
            # 处理新建文件操作
            shouldSave = self.shouldSave()
            if shouldSave == QMessageBox.Save:
                self.save()
            elif shouldSave == QMessageBox.Cancel:
                return

            # 清空编辑器历史记录
            EditorHistory().clear()
            # 获取历史工具实例并调用清空方法
            historyTools = self.getRegisteredTools(classNameFilters=["HistoryTool"])
            for historyTool in historyTools:
                historyTool.onClear()
            # 创建新文件
            self.newFile()
            # 保存新文件状态到历史记录
            EditorHistory().saveState("New file")
            self.currentFileName = None
            self.modified = False
            # 更新窗口标签
            self.updateLabel()
        if currentInputAction in actionSaveVariants:
            # 处理保存文件操作
            self.save()
        if currentInputAction in actionLoadVariants:
            # 处理加载文件操作
            shouldSave = self.shouldSave()
            if shouldSave == QMessageBox.Save:
                self.save()
            elif shouldSave == QMessageBox.Discard:
                return
            self.load()
        if currentInputAction in actionSaveAsVariants:
            # 处理另存为操作
            self.save(True)

    def loadFromFileChecked(self, filePath):
        """
        检查是否需要保存文件后再从文件加载数据。

        :param filePath: 要加载的文件路径
        """
        shouldSave = self.shouldSave()
        if shouldSave == QMessageBox.Save:
            self.save()
        elif shouldSave == QMessageBox.Discard:
            return
        # 从文件加载数据
        self.loadFromFile(filePath)
        self.modified = False
        # 更新窗口标签
        self.updateLabel()

    def loadFromData(self, data, clearHistory=False):
        """
        从数据加载图信息。

        :param data: 包含图信息的数据
        :param clearHistory: 是否清空历史记录，默认为 False
        """
        # 检查要加载的所有包是否可用
        missedPackages = set()  # TODO: 目前没有填充该集合，无法报告缺失的包
        if not validateGraphDataPackages(data, missedPackages):
            # 若有缺失的包，显示错误消息框
            msg = "This graph can not be loaded. Following packages not found:\n\n"
            index = 1
            for missedPackageName in missedPackages:
                msg += "{0}. {1}\n".format(index, missedPackageName)
                index += 1
            QMessageBox.critical(self, "Missing dependencies", msg)
            return

        if clearHistory:
            # 清空编辑器历史记录
            EditorHistory().clear()
            # 获取历史工具实例并调用清空方法
            historyTools = self.getRegisteredTools(classNameFilters=["HistoryTool"])
            for historyTool in historyTools:
                historyTool.onClear()

        # 创建新文件，不保留根节点
        self.newFile(keepRoot=False)
        # 反序列化图数据
        self.graphManager.get().deserialize(data)
        # 发射文件加载完成信号
        self.fileBeenLoaded.emit()
        # 选择指定名称的图
        self.graphManager.get().selectGraphByName(data["activeGraph"])
        # 更新窗口标签
        self.updateLabel()
        # 重建路径注册表
        PathsRegistry().rebuild()

    @property
    def currentFileName(self):
        """
        获取当前文件名。

        :return: 当前文件名
        """
        return self._currentFileName

    @currentFileName.setter
    def currentFileName(self, value):
        """
        设置当前文件名，并更新窗口标签。

        :param value: 要设置的文件名
        """
        self._currentFileName = value
        self.updateLabel()

    def loadFromFile(self, filePath):
        """
        从文件加载图信息。

        :param filePath: 要加载的文件路径
        """
        with open(filePath, "r") as f:
            # 加载 JSON 数据
            data = json.load(f)
            # 从数据加载图信息，并清空历史记录
            self.loadFromData(data, clearHistory=True)
            self.currentFileName = filePath
            # 保存打开文件的状态到历史记录
            EditorHistory().saveState(
                "Open {}".format(os.path.basename(self.currentFileName))
            )

    def load(self):
        """
        弹出文件选择对话框，选择要加载的文件并加载。
        """
        name_filter = "Graph files (*.pygraph)"
        # 获取用户选择的文件路径
        savepath = QFileDialog.getOpenFileName(filter=name_filter)
        if type(savepath) in [tuple, list]:
            fpath = savepath[0]
        else:
            fpath = savepath
        if not fpath == "":
            # 从文件加载图信息
            self.loadFromFile(fpath)

    def save(self, save_as=False):
        """
        保存当前图信息到文件。

        :param save_as: 是否使用另存为模式，默认为 False
        :return: 保存是否成功的布尔值
        """
        if save_as:
            name_filter = "Graph files (*.pygraph)"
            # 获取用户选择的保存路径
            savepath = QFileDialog.getSaveFileName(filter=name_filter)
            if type(savepath) in [tuple, list]:
                pth = savepath[0]
            else:
                pth = savepath
            if not pth == "":
                self.currentFileName = pth
            else:
                self.currentFileName = None
        else:
            if self.currentFileName is None:
                name_filter = "Graph files (*.pygraph)"
                # 获取用户选择的保存路径
                savepath = QFileDialog.getSaveFileName(filter=name_filter)
                if type(savepath) in [tuple, list]:
                    pth = savepath[0]
                else:
                    pth = savepath
                if not pth == "":
                    self.currentFileName = pth
                else:
                    self.currentFileName = None

        if not self.currentFileName:
            return False

        if not self.currentFileName.endswith(".pygraph"):
            # 确保文件名以 .pygraph 结尾
            self.currentFileName += ".pygraph"

        if not self.currentFileName == "":
            # 获取当前时间并格式化为字符串
            current_time = datetime.now().strftime("%Y-%m-%d_%H_%M_%S")

            # 生成临时文件名
            tempFileName = self.currentFileName.replace(".pygraph", "") + f"-tempfile-{current_time}.pygraph"

            # 打开临时文件和目标文件
            with open(tempFileName, "w+") as tempPtr, open(self.currentFileName, "r+") as filePtr:
                try:
                    # 序列化图数据
                    saveData = self.graphManager.get().serialize()
                    # 将序列化数据写入临时文件
                    json.dump(saveData, tempPtr, indent=4)

                    # 将文件指针移动到临时文件开头
                    tempPtr.seek(0)
                    # 将临时文件内容写入目标文件
                    filePtr.write(tempPtr.read())
                    tempPtr.close()

                    # 若保存成功，删除临时文件
                    os.remove(tempFileName)

                    print(f"// saved: '{self.currentFileName}'")
                except Exception as e:
                    # 若保存失败，抛出运行时错误
                    raise RuntimeError(
                        f'JSON serialization failed.\nCould not save file \"{self.currentFileName}\"') from e

            self.modified = False
            # 更新窗口标签
            self.updateLabel()
            return True

    def _clickNewFile(self):
        """
        处理新建文件菜单项点击事件。
        """
        shouldSave = self.shouldSave()
        if shouldSave == QMessageBox.Save:
            self.save()
        elif shouldSave == QMessageBox.Cancel:
            return

        # 清空编辑器历史记录
        EditorHistory().clear()
        # 获取历史工具实例并调用清空方法
        historyTools = self.getRegisteredTools(classNameFilters=["HistoryTool"])
        for historyTool in historyTools:
            historyTool.onClear()
        # 创建新文件
        self.newFile()
        # 保存新文件状态到历史记录
        EditorHistory().saveState("New file")
        self.currentFileName = None
        self.modified = False
        # 更新窗口标签
        self.updateLabel()

    def newFile(self, keepRoot=True):
        """
        创建新文件，停止主循环，清空图信息，并重新启动主循环。

        :param keepRoot: 是否保留根节点，默认为 True
        """
        # 停止定时器
        self.tick_timer.stop()
        self.tick_timer.timeout.disconnect()

        # 广播信号，清空图信息
        self.graphManager.get().clear(keepRoot=keepRoot)
        # 发射新文件创建信号
        self.newFileExecuted.emit(keepRoot)
        # 请求清空属性工具内容
        self.onRequestClearProperties()

        # 启动主循环
        self.startMainLoop()

    def startMainLoop(self):
        """
        启动主循环，连接定时器的超时信号到主循环函数，并启动定时器。
        """
        self.tick_timer.timeout.connect(self.mainLoop)
        self.tick_timer.start(1000 / EDITOR_TARGET_FPS)

    def stopMainLoop(self):
        """
        停止主循环，停止定时器并断开超时信号连接。
        """
        self.tick_timer.stop()
        self.tick_timer.timeout.disconnect()

    def mainLoop(self):
        """
        主循环函数，处理异步任务、更新帧率、更新图和画布。
        """
        # 运行异步任务
        asyncio.get_event_loop().run_until_complete(self._tick_asyncio())

        # 计算时间差
        deltaTime = currentProcessorTime() - self._lastClock
        ds = deltaTime * 1000.0
        if ds > 0:
            # 计算帧率
            self.fps = int(1000.0 / ds)

        # 更新所有图
        # 每个图将更新其拥有的原始节点
        # 每个原始节点将更新其 UI 包装器（如果存在）
        self.graphManager.get().Tick(deltaTime)

        # 更新画布，仅更新 UI 相关的内容，如动画等
        self.canvasWidget.Tick(deltaTime)

        # 更新上一次时钟时间
        self._lastClock = currentProcessorTime()

    async def _tick_asyncio(self):
        """
        异步任务，等待极短时间。
        """
        await asyncio.sleep(0.00001)

    def createPopupMenu(self):
        """
        创建弹出菜单，当前为空实现。
        """
        pass

    def getToolClassByName(self, packageName, toolName, toolClass=DockTool):
        """
        根据包名、工具名和工具类类型获取工具类。

        :param packageName: 包名
        :param toolName: 工具名
        :param toolClass: 工具类类型，默认为 DockTool
        :return: 找到的工具类，若未找到则返回 None
        """
        registeredTools = GET_TOOLS()
        for ToolClass in registeredTools[packageName]:
            if issubclass(ToolClass, toolClass):
                if ToolClass.name() == toolName:
                    return ToolClass
        return None

    def createToolInstanceByClass(self, packageName, toolName, toolClass=DockTool):
        """
        根据包名、工具名和工具类类型创建工具实例。

        :param packageName: 包名
        :param toolName: 工具名
        :param toolClass: 工具类类型，默认为 DockTool
        :return: 创建的工具实例，若未创建成功则返回 None
        """
        registeredTools = GET_TOOLS()
        for ToolClass in registeredTools[packageName]:
            # 获取工具支持的软件列表
            supportedSoftwares = ToolClass.supportedSoftwares()
            if "any" not in supportedSoftwares:
                if self.currentSoftware not in supportedSoftwares:
                    continue

            if issubclass(ToolClass, toolClass):
                if ToolClass.name() == toolName:
                    return ToolClass()
        return None

    def getRegisteredTools(self, classNameFilters=None):
        """
        获取已注册的工具实例。

        :param classNameFilters: 类名过滤器列表，默认为 None
        :return: 符合过滤器条件的工具实例列表
        """
        if classNameFilters is None:
            classNameFilters = []
        if len(classNameFilters) == 0:
            return self._tools
        else:
            result = []
            for tool in self._tools:
                if tool.__class__.__name__ in classNameFilters:
                    result.append(tool)
            return result

    def invokeDockToolByName(self, packageName, name, settings=None):
        """
        根据包名和工具名调用停靠工具。
        如果提供了设置，将调用 QMainWindow::restoreDockWidget 而不是 QMainWindow::addDockWidget。

        :param packageName: 包名
        :param name: 工具名
        :param settings: 工具设置，默认为 None
        :return: 创建或显示的工具实例，若未成功则返回 None
        """
        # 根据包名和工具名获取工具类
        toolClass = self.getToolClassByName(packageName, name, DockTool)
        if toolClass is None:
            return
        # 检查工具是否为单例
        isSingleton = toolClass.isSingleton()
        if isSingleton:
            # 检查工具是否已经注册
            if name in [t.name() for t in self._tools]:
                for tool in self._tools:
                    if tool.name() == name:
                        # 显示工具并调用显示方法
                        tool.show()
                        tool.onShow()
                        # 高亮显示窗口
                        # print("highlight", tool.uniqueName())
                        return tool
        # 创建工具实例
        ToolInstance = self.createToolInstanceByClass(packageName, name, DockTool)
        if ToolInstance:
            # 注册工具实例
            self.registerToolInstance(ToolInstance)
            if settings is not None:
                # 恢复工具状态
                ToolInstance.restoreState(settings)
                if not self.restoreDockWidget(ToolInstance):
                    # 处理 UI 状态未恢复的情况
                    pass
            else:
                # 将工具添加到停靠区域
                self.addDockWidget(ToolInstance.defaultDockArea(), ToolInstance)
            # 设置应用程序实例
            ToolInstance.setAppInstance(self)
            # 调用工具的显示方法
            ToolInstance.onShow()
        return ToolInstance

    def shouldSave(self):
        """
        判断是否需要保存文件，若文件被修改则弹出确认对话框。

        :return: 用户选择的按钮，如 QMessageBox.Save、QMessageBox.Cancel 等
        """
        if self.modified:
            # 若文件被修改，显示警告消息框
            btn = QMessageBox.warning(
                self,
                "Confirm?",
                "Unsaved data will be lost. Save?",
                QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard,
            )
            return btn
        return QMessageBox.Discard

    def closeEvent(self, event):
        """
        处理窗口关闭事件，检查是否需要保存文件，保存编辑器配置，清理资源。

        :param event: 关闭事件对象
        """
        shouldSave = self.shouldSave()
        if shouldSave == QMessageBox.Save:
            if not self.save():
                # 若保存失败，忽略关闭事件
                event.ignore()
                return
        elif shouldSave == QMessageBox.Cancel:
            # 若用户取消，忽略关闭事件
            event.ignore()
            return

        # 停止定时器
        self.tick_timer.stop()
        self.tick_timer.timeout.disconnect()
        # 关闭编辑器历史记录
        EditorHistory().shutdown()

        # 关闭画布部件
        self.canvasWidget.shoutDown()
        # 保存编辑器配置
        settings = ConfigManager().getSettings("APP_STATE")

        # 清空文件，以捕获打开的停靠工具
        settings.clear()
        settings.sync()

        settings.beginGroup("Editor")
        # 保存窗口几何信息
        settings.setValue("geometry", self.saveGeometry())
        # 保存窗口状态信息
        settings.setValue("state", self.saveState())
        settings.endGroup()

        # 保存工具状态
        settings.beginGroup("Tools")
        for tool in self._tools:
            if isinstance(tool, ShelfTool):
                settings.beginGroup("ShelfTools")
                settings.beginGroup(tool.name())
                # 保存工具状态
                tool.saveState(settings)
                settings.endGroup()
                settings.endGroup()
            if isinstance(tool, DockTool):
                settings.beginGroup("DockTools")
                settings.beginGroup(tool.uniqueName())
                # 保存工具状态
                tool.saveState(settings)
                settings.endGroup()
                settings.endGroup()
            # 调用工具的销毁方法
            tool.onDestroy()
        settings.endGroup()
        settings.sync()

        # 删除临时目录（如果存在）
        if os.path.exists(self.currentTempDir):
            shutil.rmtree(self.currentTempDir)

        # 销毁所有单例实例
        SingletonDecorator.destroyAll()

        # 清空应用程序实例引用
        PySC.appInstance = None

        # 调用父类的关闭事件处理方法
        QMainWindow.closeEvent(self, event)

    def _clickReloadPackages(self):
        """
        处理重新加载包菜单项点击事件，重新初始化包。
        """
        print("Reloading packages")
        extraPackagePaths = []
        # 从配置管理器中获取额外的包目录路径
        extraPathsString = ConfigManager().getPrefsValue(
            "PREFS", "General/ExtraPackageDirs"
        )
        if extraPathsString is not None:
            # 去除路径末尾的分号
            extraPathsString = extraPathsString.rstrip(";")
            # 分割路径字符串
            extraPathsRaw = extraPathsString.split(";")
            for rawPath in extraPathsRaw:
                if os.path.exists(rawPath):
                    # 规范化路径并添加到列表中
                    extraPackagePaths.append(os.path.normpath(rawPath))
        # 重新初始化包
        INITIALIZE(additionalPackageLocations=extraPackagePaths, software="standalone")

    @staticmethod
    def instance(parent=None, software=""):
        """
        获取 PyFlow 应用程序的实例，若实例不存在则创建一个新实例。

        :param parent: 父窗口对象，默认为 None
        :param software: 运行的软件名称，不能为空
        :return: PyFlow 应用程序实例
        """
        assert (
                software != ""
        ), "Invalid arguments. Please pass you software name as second argument!"
        # 获取应用程序状态设置
        settings = ConfigManager().getSettings("APP_STATE")

        # 创建 PyFlow 应用程序实例
        instance = PySC(parent)
        instance.currentSoftware = software
        # 设置会话描述符的软件名称
        SessionDescriptor().software = instance.currentSoftware

        if software == "standalone":
            # 若为独立运行模式，设置可编辑样式表
            editableStyleSheet(instance)

        try:
            extraPackagePaths = []
            # 从配置管理器中获取额外的包目录路径
            extraPathsString = ConfigManager().getPrefsValue(
                "PREFS", "General/ExtraPackageDirs"
            )
            if extraPathsString is not None:
                # 去除路径末尾的分号
                extraPathsString = extraPathsString.rstrip(";")
                # 分割路径字符串
                extraPathsRaw = extraPathsString.split(";")
                for rawPath in extraPathsRaw:
                    if os.path.exists(rawPath):
                        # 规范化路径并添加到列表中
                        extraPackagePaths.append(os.path.normpath(rawPath))
            # 初始化包
            INITIALIZE(additionalPackageLocations=extraPackagePaths, software=software)
        except Exception as e:
            # 若初始化失败，显示错误消息框
            QMessageBox.critical(None, "Fatal error", str(e))
            return

        # 启动主循环
        instance.startMainLoop()

        # 填充工具栏
        toolbar = instance.getToolbar()

        # 填充菜单栏
        instance.populateMenu()

        # 恢复窗口几何信息
        geo = settings.value("Editor/geometry")
        if geo is not None:
            instance.restoreGeometry(geo)
        # 恢复窗口状态信息
        state = settings.value("Editor/state")
        if state is not None:
            instance.restoreState(state)
        settings.beginGroup("Tools")
        for packageName, registeredToolSet in GET_TOOLS().items():
            for ToolClass in registeredToolSet:
                if issubclass(ToolClass, ShelfTool):
                    # 创建货架工具实例
                    ToolInstance = ToolClass()
                    # 注册工具实例，防止被垃圾回收
                    instance.registerToolInstance(ToolInstance)
                    # 设置应用程序实例
                    ToolInstance.setAppInstance(instance)
                    # 创建动作对象
                    action = QAction(instance)
                    action.setIcon(ToolInstance.getIcon())
                    action.setText(ToolInstance.name())
                    action.setToolTip(ToolInstance.toolTip())
                    action.setObjectName(ToolInstance.name())
                    # 连接动作触发信号到工具的执行方法
                    action.triggered.connect(ToolInstance.do)
                    # 检查是否有上下文菜单数据
                    menuBuilder = ToolInstance.contextMenuBuilder()
                    if menuBuilder:
                        # 生成上下文菜单
                        menuGenerator = ContextMenuGenerator(menuBuilder)
                        menu = menuGenerator.generate()
                        action.setMenu(menu)
                    # 将动作添加到工具栏
                    toolbar.addAction(action)

                    # 进入 ShelfTools/ToolName 组并传递设置
                    settings.beginGroup("ShelfTools")
                    settings.beginGroup(ToolClass.name())
                    # 恢复工具状态
                    ToolInstance.restoreState(settings)
                    settings.endGroup()
                    settings.endGroup()

                if issubclass(ToolClass, DockTool):
                    # 获取菜单栏中的所有菜单
                    menus = instance.menuBar.findChildren(QMenu)
                    # 获取插件菜单的动作
                    pluginsMenuAction = [m for m in menus if m.title() == "Plugins"][0].menuAction()
                    # 获取或创建工具菜单
                    toolsMenu = getOrCreateMenu(instance.menuBar, "Tools")
                    # 在插件菜单前插入工具菜单
                    instance.menuBar.insertMenu(pluginsMenuAction, toolsMenu)
                    # 获取或创建包子菜单
                    packageSubMenu = getOrCreateMenu(toolsMenu, packageName)
                    toolsMenu.addMenu(packageSubMenu)
                    # 创建显示工具的动作
                    showToolAction = packageSubMenu.addAction(ToolClass.name())
                    icon = ToolClass.getIcon()
                    if icon:
                        showToolAction.setIcon(icon)
                    # 连接动作触发信号到调用停靠工具的方法
                    showToolAction.triggered.connect(
                        lambda x=0, pkgName=packageName, toolName=ToolClass.name(): instance.invokeDockToolByName(
                            packageName=pkgName, name=toolName
                        )
                    )

                    settings.beginGroup("DockTools")
                    # 获取所有子组
                    childGroups = settings.childGroups()
                    for dockToolGroupName in childGroups:
                        # 该停靠工具数据在上次关闭时已保存
                        settings.beginGroup(dockToolGroupName)
                        if dockToolGroupName in [
                            t.uniqueName() for t in instance._tools
                        ]:
                            settings.endGroup()
                            continue
                        # 提取工具名
                        toolName = dockToolGroupName.split("::")[0]
                        # 调用停靠工具
                        instance.invokeDockToolByName(packageName, toolName, settings)
                        settings.endGroup()
                    settings.endGroup()

        # 设置应用程序实例
        PySC.appInstance = instance
        # 保存新文件状态到历史记录
        EditorHistory().saveState("New file")

        for name, package in GET_PACKAGES().items():
            # 获取包的首选项部件
            prefsWidgets = package.PrefsWidgets()
            if prefsWidgets is not None:
                for categoryName, widgetClass in prefsWidgets.items():
                    # 向首选项窗口添加类别和部件
                    PreferencesWindow().addCategory(categoryName, widgetClass())
                # 选择名为 "General" 的类别
                PreferencesWindow().selectByName("General")
        return instance